package cn.gotom.svg;

import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.RectF;
import cn.gotom.svg.xml.StyleAttribute;

/**
 * @author 裴绍国
 * @author <a href="mailto:peixere@gmail.com">裴绍国</a>
 */
public class PathElement extends RenderableElement
{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	protected final android.graphics.Path path = new android.graphics.Path();

	public PathElement()
	{
	}

	protected void build() throws SVGException
	{
		super.build();
	}

	public void render(Canvas canvas) throws SVGException
	{
		if (this.visible())
		{
			beginLayer(canvas);
			renderPath(canvas);
			finishLayer(canvas);
		}
	}

	private void renderPath(Canvas canvas)
	{
		StyleAttribute sty = new StyleAttribute();
		setFillType();
		if (getPres(sty.setName("d")))
		{
			doPath(sty.getStringValue());
		}
		doFillPaint();
		canvas.drawPath(path, paint);
		if (doStrokePaint())
		{
			canvas.drawPath(path, paint);
		}
	}

	protected void setFillType()
	{
		StyleAttribute attr = new StyleAttribute();
		String fillRuleStrn = (this.getAttribute(attr.setName(StyleAttribute.fillRule))) ? attr.getStringValue() : "nonzero";
		path.setFillType(Path.FillType.WINDING);
		if (fillRuleStrn.equals("evenodd"))
		{
			path.setFillType(Path.FillType.EVEN_ODD);
		}
		else if (fillRuleStrn.equals("nonzero"))
		{
			path.setFillType(Path.FillType.WINDING);
		}
		else if (fillRuleStrn.equals("nonzero"))
		{
			path.setFillType(Path.FillType.WINDING);
		}
		else if (fillRuleStrn.equals("inherit"))
		{
			path.setFillType(Path.FillType.INVERSE_WINDING);
		}
	}

	private android.graphics.Path doPath(String s)
	{
		int n = s.length();
		ParserHelper ph = new ParserHelper(s, 0);
		ph.skipWhitespace();
		float lastX = 0;
		float lastY = 0;
		float lastX1 = 0;
		float lastY1 = 0;
		float subPathStartX = 0;
		float subPathStartY = 0;
		char prevCmd = 0;
		while (ph.pos < n)
		{
			char cmd = s.charAt(ph.pos);
			switch (cmd)
			{
				case '-':
				case '+':
				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
				case '8':
				case '9':
					if (prevCmd == 'm' || prevCmd == 'M')
					{
						cmd = (char) (((int) prevCmd) - 1);
						break;
					}
					else if (prevCmd == 'c' || prevCmd == 'C')
					{
						cmd = prevCmd;
						break;
					}
					else if (prevCmd == 'l' || prevCmd == 'L')
					{
						cmd = prevCmd;
						break;
					}
				default:
				{
					ph.advance();
					prevCmd = cmd;
				}
			}

			boolean wasCurve = false;
			switch (cmd)
			{
				case 'M':
				case 'm':
				{
					float x = ph.nextFloat();
					float y = ph.nextFloat();
					if (cmd == 'm')
					{
						subPathStartX += x;
						subPathStartY += y;
						path.rMoveTo(x, y);
						lastX += x;
						lastY += y;
					}
					else
					{
						subPathStartX = x;
						subPathStartY = y;
						path.moveTo(x, y);
						lastX = x;
						lastY = y;
					}
					break;
				}
				case 'Z':
				case 'z':
				{
					path.close();
					path.moveTo(subPathStartX, subPathStartY);
					lastX = subPathStartX;
					lastY = subPathStartY;
					lastX1 = subPathStartX;
					lastY1 = subPathStartY;
					wasCurve = true;
					break;
				}
				case 'L':
				case 'l':
				{
					float x = ph.nextFloat();
					float y = ph.nextFloat();
					if (cmd == 'l')
					{
						path.rLineTo(x, y);
						lastX += x;
						lastY += y;
					}
					else
					{
						path.lineTo(x, y);
						lastX = x;
						lastY = y;
					}
					break;
				}
				case 'H':
				case 'h':
				{
					float x = ph.nextFloat();
					if (cmd == 'h')
					{
						path.rLineTo(x, 0);
						lastX += x;
					}
					else
					{
						path.lineTo(x, lastY);
						lastX = x;
					}
					break;
				}
				case 'V':
				case 'v':
				{
					float y = ph.nextFloat();
					if (cmd == 'v')
					{
						path.rLineTo(0, y);
						lastY += y;
					}
					else
					{
						path.lineTo(lastX, y);
						lastY = y;
					}
					break;
				}
				case 'C':
				case 'c':
				{
					wasCurve = true;
					float x1 = ph.nextFloat();
					float y1 = ph.nextFloat();
					float x2 = ph.nextFloat();
					float y2 = ph.nextFloat();
					float x = ph.nextFloat();
					float y = ph.nextFloat();
					if (cmd == 'c')
					{
						x1 += lastX;
						x2 += lastX;
						x += lastX;
						y1 += lastY;
						y2 += lastY;
						y += lastY;
					}
					path.cubicTo(x1, y1, x2, y2, x, y);
					lastX1 = x2;
					lastY1 = y2;
					lastX = x;
					lastY = y;
					break;
				}
				case 'S':
				case 's':
				{
					wasCurve = true;
					float x2 = ph.nextFloat();
					float y2 = ph.nextFloat();
					float x = ph.nextFloat();
					float y = ph.nextFloat();
					if (cmd == 's')
					{
						x2 += lastX;
						x += lastX;
						y2 += lastY;
						y += lastY;
					}
					float x1 = 2 * lastX - lastX1;
					float y1 = 2 * lastY - lastY1;
					path.cubicTo(x1, y1, x2, y2, x, y);
					lastX1 = x2;
					lastY1 = y2;
					lastX = x;
					lastY = y;
					break;
				}
				case 'A':
				case 'a':
				{
					float rx = ph.nextFloat();
					float ry = ph.nextFloat();
					float theta = ph.nextFloat();
					int largeArc = (int) ph.nextFloat();
					int sweepArc = (int) ph.nextFloat();
					float x = ph.nextFloat();
					float y = ph.nextFloat();
					// path.arcTo(rx, ry, theta, largeArc, sweepArc, x, y);
					drawArc(path, lastX, lastY, x, y, rx, ry, theta, largeArc, sweepArc);
					lastX = x;
					lastY = y;
					break;
				}
			}
			if (!wasCurve)
			{
				lastX1 = lastX;
				lastY1 = lastY;
			}
			ph.skipWhitespace();
		}
		return path;
	}

	private void drawArc(Path path, float lastX, float lastY, float x, float y, float rx, float ry, float angle, int largeArc, int sweepArc)
	{
		if (lastX == x && lastY == y)
		{
			// If the endpoints (x, y) and (x0, y0) are identical, then this
			// is equivalent to omitting the elliptical arc segment entirely.
			// (behaviour specified by the spec)
			return;
		}

		// Handle degenerate case (behaviour specified by the spec)
		if (rx == 0 || ry == 0)
		{
			path.lineTo(x, y);
			return;
		}

		// Sign of the radii is ignored (behaviour specified by the spec)
		rx = Math.abs(rx);
		ry = Math.abs(ry);

		// Convert angle from degrees to radians
		float angleRad = (float) Math.toRadians(angle % 360.0);
		double cosAngle = Math.cos(angleRad);
		double sinAngle = Math.sin(angleRad);

		// We simplify the calculations by transforming the arc so that the origin is at the
		// midpoint calculated above followed by a rotation to line up the coordinate axes
		// with the axes of the ellipse.

		// Compute the midpoint of the line between the current and the end point
		double dx2 = (lastX - x) / 2.0;
		double dy2 = (lastY - y) / 2.0;

		// Step 1 : Compute (x1', y1') - the transformed start point
		double x1 = (cosAngle * dx2 + sinAngle * dy2);
		double y1 = (-sinAngle * dx2 + cosAngle * dy2);

		double rx_sq = rx * rx;
		double ry_sq = ry * ry;
		double x1_sq = x1 * x1;
		double y1_sq = y1 * y1;

		// Check that radii are large enough.
		// If they are not, the spec says to scale them up so they are.
		// This is to compensate for potential rounding errors/differences between SVG implementations.
		double radiiCheck = x1_sq / rx_sq + y1_sq / ry_sq;
		if (radiiCheck > 1)
		{
			rx = (float) Math.sqrt(radiiCheck) * rx;
			ry = (float) Math.sqrt(radiiCheck) * ry;
			rx_sq = rx * rx;
			ry_sq = ry * ry;
		}

		// Step 2 : Compute (cx1, cy1) - the transformed centre point
		double sign = (largeArc == sweepArc) ? -1 : 1;
		double sq = ((rx_sq * ry_sq) - (rx_sq * y1_sq) - (ry_sq * x1_sq)) / ((rx_sq * y1_sq) + (ry_sq * x1_sq));
		sq = (sq < 0) ? 0 : sq;
		double coef = (sign * Math.sqrt(sq));
		double cx1 = coef * ((rx * y1) / ry);
		double cy1 = coef * -((ry * x1) / rx);

		// Step 3 : Compute (cx, cy) from (cx1, cy1)
		double sx2 = (lastX + x) / 2.0;
		double sy2 = (lastY + y) / 2.0;
		double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
		double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);

		// Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
		double ux = (x1 - cx1) / rx;
		double uy = (y1 - cy1) / ry;
		double vx = (-x1 - cx1) / rx;
		double vy = (-y1 - cy1) / ry;
		double p, n;

		// Compute the angle start
		n = Math.sqrt((ux * ux) + (uy * uy));
		p = ux; // (1 * ux) + (0 * uy)
		sign = (uy < 0) ? -1.0 : 1.0;
		double angleStart = Math.toDegrees(sign * Math.acos(p / n));

		// Compute the angle extent
		n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
		p = ux * vx + uy * vy;
		sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
		double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
		if (sweepArc != 1 && angleExtent > 0)
		{
			angleExtent -= 360f;
		}
		else if (angleExtent < 0)
		{
			angleExtent += 360f;
		}
		angleExtent %= 360f;
		angleStart %= 360f;

		// Many elliptical arc implementations including the Java2D and Android ones, only
		// support arcs that are axis aligned. Therefore we need to substitute the arc
		// with bezier curves. The following method call will generate the beziers for
		// a unit circle that covers the arc angles we want.
		float[] bezierPoints = arcToBeziers(angleStart, angleExtent);

		// Calculate a transformation matrix that will move and scale these bezier points to the correct location.
		Matrix m = new Matrix();
		m.postScale(rx, ry);
		m.postRotate(angle);
		m.postTranslate((float) cx, (float) cy);
		m.mapPoints(bezierPoints);

		// The last point in the bezier set should match exactly the last coord pair in the arc (ie: x,y). But
		// considering all the mathematical manipulation we have been doing, it is bound to be off by a tiny
		// fraction. Experiments show that it can be up to around 0.00002. So why don't we just set it to
		// exactly what it ought to be.
		bezierPoints[bezierPoints.length - 2] = x;
		bezierPoints[bezierPoints.length - 1] = y;

		// Final step is to add the bezier curves to the path
		for (int i = 0; i < bezierPoints.length; i += 6)
		{
			path.cubicTo(bezierPoints[i], bezierPoints[i + 1], bezierPoints[i + 2], bezierPoints[i + 3], bezierPoints[i + 4], bezierPoints[i + 5]);
		}
	}

	/*
	 * Generate the control points and endpoints for a set of bezier curves that match a circular arc starting from angle 'angleStart' and sweep the angle 'angleExtent'. The circle the arc follows will be centred on (0,0) and have a radius of 1.0.
	 * 
	 * Each bezier can cover no more than 90 degrees, so the arc will be divided evenly into a maximum of four curves.
	 * 
	 * The resulting control points will later be scaled and rotated to match the final arc required.
	 * 
	 * The returned array has the format [x0,y0, x1,y1,...] and excludes the start point of the arc.
	 */
	private static float[] arcToBeziers(double angleStart, double angleExtent)
	{
		int numSegments = (int) Math.ceil(Math.abs(angleExtent) / 90.0);

		angleStart = Math.toRadians(angleStart);
		angleExtent = Math.toRadians(angleExtent);
		float angleIncrement = (float) (angleExtent / numSegments);

		// The length of each control point vector is given by the following formula.
		double controlLength = 4.0 / 3.0 * Math.sin(angleIncrement / 2.0) / (1.0 + Math.cos(angleIncrement / 2.0));

		float[] coords = new float[numSegments * 6];
		int pos = 0;

		for (int i = 0; i < numSegments; i++)
		{
			double angle = angleStart + i * angleIncrement;
			// Calculate the control vector at this angle
			double dx = Math.cos(angle);
			double dy = Math.sin(angle);
			// First control point
			coords[pos++] = (float) (dx - controlLength * dy);
			coords[pos++] = (float) (dy + controlLength * dx);
			// Second control point
			angle += angleIncrement;
			dx = Math.cos(angle);
			dy = Math.sin(angle);
			coords[pos++] = (float) (dx + controlLength * dy);
			coords[pos++] = (float) (dy - controlLength * dx);
			// Endpoint of bezier
			coords[pos++] = (float) dx;
			coords[pos++] = (float) dy;
		}
		return coords;
	}

	public RectF getBoundingBox()
	{
		RectF rect = new RectF();
		path.isRect(rect);
		return boundsToParent(includeStrokeInBounds(rect));
	}
}
